package com.fsh.anime.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fsh.anime.common.ArticleConstant;
import com.fsh.anime.common.RedisConstant;
import com.fsh.anime.mapper.*;
import com.fsh.anime.minio.FileStorageService;
import com.fsh.anime.model.dtos.ArticleDto;
import com.fsh.anime.model.dtos.PageArticleDto;
import com.fsh.anime.model.pojos.*;
import com.fsh.anime.model.pojos.Dictionary;
import com.fsh.anime.model.vos.ArticleDetailVo;
import com.fsh.anime.model.vos.PageResult;
import com.fsh.anime.model.vos.Result;
import com.fsh.anime.model.vos.UserArticleVo;
import com.fsh.anime.service.ArticleService;
import com.fsh.anime.utils.BaseContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

    @Autowired
    private ArticleContentMapper articleContentMapper;
    @Autowired
    private ArticleCoverMapper articleCoverMapper;
    @Autowired
    private ArticleMapper articleMapper;
    @Autowired
    private DictionaryMapper dictionaryMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private FileStorageService fileStorageService;
    @Autowired
    private UserArticleMapper userArticleMapper;
    @Autowired
    private UserCollectArticleMapper userCollectArticleMapper;
    @Autowired
    private UserRelationMapper userRelationMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * @param dto
     * @return {@link Result}
     * 发布文章
     * //TODO DFA过滤敏感词
     */
    @Override
    @Transactional
    public Result addArticle(ArticleDto dto) {
        //dto自动判空

        Article article=new Article();
        //拷贝属性
        BeanUtils.copyProperties(dto,article);
        //补充其他属性
        article.setUserId(BaseContext.getCurrentId().longValue());
        article.setIsPub(ArticleConstant.ARTICLE_STATUS_1);
        //1、保存到article表
        save(article);
        //2、content转码保存到 article_content表
        ArticleContent articleContent=new ArticleContent();
        articleContent.setId(article.getId());//设置id
        articleContent.setContent(HtmlUtils.htmlEscapeHex(dto.getContent()));
        articleContentMapper.insert(articleContent);
        //3、保存到article_cover表。先查询表中有没有这个封面路径
        LambdaQueryWrapper<ArticleCover> lambdaQueryWrapper=new LambdaQueryWrapper();
        lambdaQueryWrapper.eq(ArticleCover::getCover,article.getCover());
        //根据条件查询，是否有这个封面路径
        List<ArticleCover> list = articleCoverMapper.selectList(lambdaQueryWrapper);
        if (list!=null&&list.get(0).getArticleId()==null){
            ArticleCover articleCover = list.get(0);
            //有，更新关联文章的id
            articleCover.setArticleId(article.getId());
            articleCoverMapper.updateById(articleCover);
        }else{
            //没有或者已经关联了文章，直接插入。图片作为封面可以复用
            ArticleCover articleCover=new ArticleCover();
            articleCover=new ArticleCover();
            articleCover.setCover(dto.getCover());
            articleCover.setArticleId(article.getId());
            articleCoverMapper.insert(articleCover);
        }
        //更新该用户的user_article表，文章总数+1
        UserArticle userArticle = userArticleMapper.selectById(BaseContext.getCurrentId());
        userArticle.setArticleTotal(userArticle.getArticleTotal()+1);
        userArticleMapper.updateById(userArticle);

        //保存文章成功。就推送到该作者粉丝的收件箱 （redis中ZSet实现，分值为发表时间戳。存文章的id。key为：feed:粉丝的id）
        //查询笔记作者的所有粉丝 select * from user_relation where follow_id = ?
        List<UserRelation> fans = userRelationMapper.selectList(new LambdaQueryWrapper<UserRelation>().eq(UserRelation::getFollowId, BaseContext.getCurrentId()));
        fans.stream().forEach(fan->{
            //可以设置一个过期时间（1周）
            stringRedisTemplate.opsForZSet().add(RedisConstant.FEED_PRE+fan.getUserId(),article.getId().toString(),System.currentTimeMillis());
            stringRedisTemplate.expire(RedisConstant.FEED_PRE+fan.getUserId(),7,TimeUnit.DAYS);
        });

        return Result.success("发布成功");
    }

    /**
     * @param dto
     * @return {@link Result}
     * 根据分页参数获取文章列表。  特别注意，获取当前登录用户的文章。
     */
    @Override
    public Result getMyArticleList(PageArticleDto dto) {
        //自动检验参数。

        //查看当前登录的用户文章列表
        Long userId=BaseContext.getCurrentId().longValue();
        if(dto.getUserId()!=null){
            //说明是查看别人的文章列表。重新赋值
            userId=dto.getUserId();
        }

        //调用 分页方法
        PageResult pageResult = QueryByPage(dto, userId);
        return Result.success(pageResult);
    }

    /**
     * @param dto
     * @return {@link Result}
     * 根据分页参数获取广场文章列表。
     */
    @Override
    public Result getSquareArticleList(PageArticleDto dto) {
        //自动检验参数。

        //当前用户的推荐列表
        if(dto.getTypeId()==-1){
            //调用方法
            return recommendArticleList(dto);
        }


        //调用 分页方法
        PageResult pageResult = QueryByPage(dto, null);
        return Result.success(pageResult);
    }

    /**
     * 当前用户的推荐文章列表。
     * 0、先查询收件箱列表。
     * 1、优先按时间降序，查询关注作者的文章。
     * 2、没有关注过任何作者，那就查询最近一周的所有文章，按热度分数降序。
     *
     * @param dto 迪托
     * @return {@link Result}
     */
    private Result recommendArticleList(PageArticleDto dto) {
        String key=RedisConstant.FEED_PRE+BaseContext.getCurrentId();
        //获取收件箱 文章列表
        Set<String> articleIds = stringRedisTemplate.opsForZSet().reverseRangeByScore(key, 0, System.currentTimeMillis());
        if(articleIds.isEmpty()){
            PageResult pageResult=new PageResult();
            pageResult.setTotal(0L);
            pageResult.setRecords(Collections.emptyList());
            return Result.success(pageResult);
        }
        //获取id
        List<Long> ids=new ArrayList<>(articleIds.size());
        for (String articleId : articleIds) {
            ids.add(Long.valueOf(articleId));
        }

        //TODO 分页查询   推荐文章
        //这里先全部查询出来。
        String idStr = StrUtil.join(",", articleIds);
//        List<Article> articleList = query().in("id", ids).orderByDesc("pub_time").list();
        List<Article> articleList = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();

        //调用公共方法
        List<UserArticleVo> list = getNameAndType(null, articleList);

        PageResult pageResult = new PageResult();
//        pageResult.setTotal(2L);
        pageResult.setRecords(Arrays.asList(list.toArray()));
        return Result.success(pageResult);
    }

    /**
     * 公共分页方法。
     *
     * @param dto    迪托
     * @param userId 用户标识
     * @return {@link PageResult}
     */
    private PageResult QueryByPage(PageArticleDto dto,Long userId){

        IPage page=new Page(dto.getPageNum(),dto.getPageSize());
        LambdaQueryWrapper<Article> lambdaQueryWrapper=new LambdaQueryWrapper<>();
        if(userId!=null){
            lambdaQueryWrapper.eq(Article::getUserId,userId);
        }
        lambdaQueryWrapper
                .eq(dto.getTypeId()!=null,Article::getTypeId,dto.getTypeId())
                .eq(dto.getIsPub()!=null,Article::getIsPub,dto.getIsPub())
                .like(StringUtils.isNotBlank(dto.getKeywords()),Article::getTitle,dto.getKeywords())
                .orderByDesc(StringUtils.isNotBlank(dto.getSortType())&&dto.getSortType().equals("pub_time"),Article::getPubTime)
                .orderByDesc(StringUtils.isNotBlank(dto.getSortType())&&dto.getSortType().equals("likes"),Article::getLikes)
                .orderByDesc(StringUtils.isNotBlank(dto.getSortType())&&dto.getSortType().equals("comments"),Article::getComments)
                .orderByDesc(StringUtils.isNotBlank(dto.getSortType())&&dto.getSortType().equals("reads"),Article::getReads);

        IPage page1 = articleMapper.selectPage(page,lambdaQueryWrapper);
        //调用公共方法
        List<UserArticleVo> list = getNameAndType(page1,null);

        PageResult pageResult=new PageResult();
        pageResult.setRecords(Arrays.asList(list.toArray()));
        pageResult.setTotal(page1.getTotal());
        return pageResult;
    }

    /**
     * 公共方法：
     * 主要转换两个字段，type专栏分类id-》文字 ， name作者id-》文字
     *
     * @param page1 页1
     * @return {@link List}<{@link UserArticleVo}>
     */
    private List<UserArticleVo> getNameAndType(IPage page1,List<Article> articleList){
        //查询出所有的专栏分类
        LambdaQueryWrapper<Dictionary> lambdaQueryWrapper1=new LambdaQueryWrapper<>();
        lambdaQueryWrapper1.eq(Dictionary::getType,6);
        List<Dictionary> dictionaries = dictionaryMapper.selectList(lambdaQueryWrapper1);

        List<Article> records=null;
        if(articleList==null){
            records =(List<Article>) page1.getRecords();
        }else{
            records=articleList;
        }
        //article文章列表

        //转换成 UserArticleVo文章列表 。  主要转换两个字段，type专栏分类id-》文字 ， name作者id-》文字
        List<UserArticleVo> list = records.stream().map(item -> {
            UserArticleVo userArticleVo=new UserArticleVo();
            BeanUtils.copyProperties(item,userArticleVo);
            //type专栏分类id-》文字
            dictionaries.stream()
                    .filter(d->d.getId()==item.getTypeId())
                    .forEach(d->userArticleVo.setType(d.getLabel()));
            //name作者id-》文字
            User user = userMapper.selectOne(new QueryWrapper<User>().eq("id", item.getUserId()));
            userArticleVo.setName(user.getName());
            return userArticleVo;
        }).collect(Collectors.toList());

        return list;
    }


    /**
     * 获取一篇文章的详情
     *
     * @param id 文章id
     * @return {@link Result}
     */
    @Override
    public Result getOneArticle(Long id) {
        if(id==null){
            return Result.error("请求参数错误");
        }
        //TODO 可以多表联查
        ArticleDetailVo articleDetailVo = new ArticleDetailVo();
        //查询article表
        Article article = articleMapper.selectById(id);
        BeanUtils.copyProperties(article,articleDetailVo);
        //查询user_article表
        UserArticle userArticle = userArticleMapper.selectOne(new LambdaQueryWrapper<UserArticle>().eq(UserArticle::getUserId, article.getUserId()));
        BeanUtils.copyProperties(userArticle,articleDetailVo);
        //查询article_content
        ArticleContent articleContent = articleContentMapper.selectOne(new LambdaQueryWrapper<ArticleContent>().eq(ArticleContent::getId, id));
        //解码反编译content
        String content = HtmlUtils.htmlUnescape(articleContent.getContent());
        articleDetailVo.setContent(content);

        //TODO 以下可以 使用 异步

        //判断当前用户是否对该文章 点过赞。
        Boolean member = stringRedisTemplate.opsForSet().isMember(RedisConstant.ARTICLE_LIKES_PRE + id, BaseContext.getCurrentId().toString());
        articleDetailVo.setLikesUp(BooleanUtil.isTrue(member)?1:0);

        //判断当前用户是否 收藏过 该文章。
        String key=RedisConstant.USER_COLLECT_ARTICLE + BaseContext.getCurrentId();
        Boolean member1 = stringRedisTemplate.opsForSet().isMember(key, id.toString());
        if(BooleanUtil.isTrue(member1)){
            //在redis中找到了 ，说明收藏过。
            articleDetailVo.setCollectUp(1);
        }else{
            //redis中没有找到 ，那么从MySQL中找。
            UserCollectArticle userCollectArticle = userCollectArticleMapper.selectOne(new LambdaQueryWrapper<UserCollectArticle>().eq(UserCollectArticle::getUserId, BaseContext.getCurrentId()).eq(UserCollectArticle::getArticleId, id));
            if(userCollectArticle!=null){
                //找到了，说明收藏过。
                articleDetailVo.setCollectUp(1);
                //再放入到redis中 , 并设置过期时间8h
                stringRedisTemplate.opsForSet().add(key,id.toString());
                stringRedisTemplate.expire(key,8, TimeUnit.HOURS);
            }else{
                //没找到，说明没有收藏过
                articleDetailVo.setCollectUp(0);
            }
        }

        //判断当前登录用户是否  关注过这篇文章的作者
        String key2=RedisConstant.USER_FOLLOW_PRE+BaseContext.getCurrentId();
        //根据文章id查询作者的id
        Article article1 = articleMapper.selectOne(new QueryWrapper<Article>().select("user_id").eq("id", id));
        //先在redis中查询
        Boolean member2 = stringRedisTemplate.opsForSet().isMember(key2, article1.getUserId().toString());
        if(BooleanUtil.isTrue(member2)){
            ///在redis中找到了 ，说明关注过。
            articleDetailVo.setFollowsUp(1);
        }else{
            //redis中没有找到 ，那么从MySQL中找。
            UserRelation userRelation = userRelationMapper.selectOne(new LambdaQueryWrapper<UserRelation>().eq(UserRelation::getUserId, BaseContext.getCurrentId()).eq(UserRelation::getFollowId, article1.getUserId()));
            if(userRelation!=null){
                //找到了，说明关注过。
                articleDetailVo.setFollowsUp(1);
                //再放入到redis中 , 并设置过期时间2h
                stringRedisTemplate.opsForSet().add(key2,userRelation.getFollowId().toString());
                stringRedisTemplate.expire(key2,2, TimeUnit.HOURS);
            }else {
                //没找到，说明没有关注过
                articleDetailVo.setCollectUp(0);
            }
        }
        //返回文章
        return Result.success(articleDetailVo);
    }

    /**
     * @param file
     * @return {@link String}
     * 上传文章 封面 的图片 . 并保存到 article_cover表
     */
    @Override
    public String uploadImg(MultipartFile file) {
        //获取原始文件名
        String originalFilename = file.getOriginalFilename();
        //获取文件的扩展名（截取字符串）
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        //用uuid构造新的文件名+扩展名
        String newFileName= UUID.randomUUID().toString()+extension;
//        System.out.println(newFileName);
        String imgPath=null;
        InputStream is = null;
        //上传图片到minio
        try {
            is = file.getInputStream();
            imgPath = fileStorageService.uploadImgFile("", newFileName,is);

            //保存到article_cover表
            ArticleCover articleCover = new ArticleCover();
            articleCover.setCover(imgPath);
            articleCoverMapper.insert(articleCover);
        } catch (IOException e) {
            e.printStackTrace();
            return "文件上传失败";
        }finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return imgPath;
    }

    /**
     * 文章点赞
     *
     * @param articleId 文章编号
     * @param likesUp   点赞标识
     * @return {@link Result}
     */
    @Override
    @Transactional
    public Result likeArticle(String articleId, Integer likesUp) {
        if(articleId==null || likesUp==null){
            return Result.error("点赞失败");
        }
        //获取当前用户的id
        Integer userId = BaseContext.getCurrentId();
        //redis中key
        String key= RedisConstant.ARTICLE_LIKES_PRE+articleId;
        //判断行为。 1是点赞 ，0是取消点赞
        if(likesUp==1){
            //点赞。 先操作数据库，再操作redis( TODO 可以设置过期时间)
            int count = articleMapper.update(null, new UpdateWrapper<Article>().setSql("likes=likes+1").eq("id", articleId));
            if (count>0){
                stringRedisTemplate.opsForSet().add(key,userId.toString());
            }
        }else{
            //0是取消点赞。先操作数据库，再操作redis
            int count = articleMapper.update(null, new UpdateWrapper<Article>().setSql("likes=likes-1").eq("id", articleId));
            if (count>0){
                stringRedisTemplate.opsForSet().remove(key,userId.toString());
            }
        }
        //返回该文章的点赞量
        Article article = articleMapper.selectOne(new QueryWrapper<Article>().select("likes").eq("id", articleId));
        return Result.success(article.getLikes());
    }

    /**
     * 收藏文章
     *
     * @param articleId 文章编号
     * @param collectUp 收藏行为
     * @return {@link Result}
     */
    @Override
    @Transactional
    public Result collectArticle(Long articleId, Integer collectUp) {
        if(articleId==null || collectUp==null){
            return Result.error("收藏失败");
        }
        //获取当前用户的id
        Integer userId = BaseContext.getCurrentId();
        //redis中key
        String key= RedisConstant.USER_COLLECT_ARTICLE+userId;
        //判断行为。 1是收藏 ，0是取消收藏
        if(collectUp==1){
            //收藏。 先操作数据库，再操作redis（过期时间设置8小时）
            int count = userCollectArticleMapper.insert(new UserCollectArticle(userId.longValue(), articleId));
            //该文章的收藏量+1
            int count2 = articleMapper.update(null, new UpdateWrapper<Article>().setSql("collects=collects+1").eq("id", articleId));
            if (count+count2>1){
                stringRedisTemplate.opsForSet().add(key,articleId.toString());
                stringRedisTemplate.expire(key,8, TimeUnit.HOURS);
            }
        }else{
            //取消收藏。 数据库先删除，redis再删除
            LambdaQueryWrapper<UserCollectArticle> lambdaQueryWrapper=new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(UserCollectArticle::getUserId,userId)
                    .eq(UserCollectArticle::getArticleId,articleId);
            int count = userCollectArticleMapper.delete(lambdaQueryWrapper);
            int count2 = articleMapper.update(null, new UpdateWrapper<Article>().setSql("collects=collects-1").eq("id", articleId));
            if (count+count2>0){
                stringRedisTemplate.opsForSet().remove(key,articleId.toString());
            }
        }
        //返回该文章的收藏量
        Article article = articleMapper.selectOne(new QueryWrapper<Article>().select("collects").eq("id", articleId));
        return Result.success(article.getCollects());
    }
}

